/*
 * Copyright (C) 2016 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package okhttp3.internal.http;

import static okhttp3.internal.Util.checkDuration;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Connection;
import okhttp3.EventListener;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.connection.RealConnection;
import okhttp3.internal.connection.StreamAllocation;

/**
 * A concrete interceptor chain that carries the entire interceptor chain: all
 * application interceptors, the OkHttp core, all network interceptors, and
 * finally the network caller.
 */
public final class RealInterceptorChain implements Interceptor.Chain {
	private final List<Interceptor> interceptors;
	private final StreamAllocation streamAllocation;
	private final HttpCodec httpCodec;
	private final RealConnection connection;
	private final int index;
	private final Request request;
	private final Call call;
	private final EventListener eventListener;
	private final int connectTimeout;
	private final int readTimeout;
	private final int writeTimeout;
	private int calls;

	public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation, HttpCodec httpCodec,
			RealConnection connection, int index, Request request, Call call, EventListener eventListener,
			int connectTimeout, int readTimeout, int writeTimeout) {
		this.interceptors = interceptors;
		this.connection = connection;
		this.streamAllocation = streamAllocation;
		this.httpCodec = httpCodec;
		this.index = index;
		this.request = request;
		this.call = call;
		this.eventListener = eventListener;
		this.connectTimeout = connectTimeout;
		this.readTimeout = readTimeout;
		this.writeTimeout = writeTimeout;
	}

	@Override
	public Connection connection() {
		return connection;
	}

	@Override
	public int connectTimeoutMillis() {
		return connectTimeout;
	}

	@Override
	public Interceptor.Chain withConnectTimeout(int timeout, TimeUnit unit) {
		int millis = checkDuration("timeout", timeout, unit);
		return new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index, request, call,
				eventListener, millis, readTimeout, writeTimeout);
	}

	@Override
	public int readTimeoutMillis() {
		return readTimeout;
	}

	@Override
	public Interceptor.Chain withReadTimeout(int timeout, TimeUnit unit) {
		int millis = checkDuration("timeout", timeout, unit);
		return new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index, request, call,
				eventListener, connectTimeout, millis, writeTimeout);
	}

	@Override
	public int writeTimeoutMillis() {
		return writeTimeout;
	}

	@Override
	public Interceptor.Chain withWriteTimeout(int timeout, TimeUnit unit) {
		int millis = checkDuration("timeout", timeout, unit);
		return new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index, request, call,
				eventListener, connectTimeout, readTimeout, millis);
	}

	public StreamAllocation streamAllocation() {
		return streamAllocation;
	}

	public HttpCodec httpStream() {
		return httpCodec;
	}

	@Override
	public Call call() {
		return call;
	}

	public EventListener eventListener() {
		return eventListener;
	}

	@Override
	public Request request() {
		return request;
	}

	@Override
	public Response proceed(Request request) throws IOException {
		return proceed(request, streamAllocation, httpCodec, connection);
	}

	public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
		// index的初始值是0
		if (index >= interceptors.size())
			throw new AssertionError();

		calls++;

		// If we already have a stream, confirm that the incoming request will use it.
		if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
			throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must retain the same host and port");
		}

		// If we already have a stream, confirm that this is the only call to
		// chain.proceed().
		// 保证此次调用的唯一性
		if (this.httpCodec != null && calls > 1) {
			throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must call proceed() exactly once");
		}

		// 获取链中的下一个拦截器
		// 注意到这里使用责任链进行处理的时候，会新建下一个责任链并把index+1作为下一个责任链的index。然后，我们使用index
		// 从拦截器列表中取出一个拦截器，调用它的 intercept() 方法，并把下一个执行链作为参数传递进去
		// 这样，当下一个拦截器希望自己的下一级继续处理这个请求的时候，可以调用传入的责任链的 proceed()
		// 方法；如果自己处理完毕之后，下一级不需要继续处理，那么就直接返回一个 Response 实例即可。因为，每次都是在当前的 index 基础上面加
		// 1，所以能在调用 proceed() 的时候准确地从拦截器列表中取出下一个拦截器进行处理
		RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection,
				index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
		// 获取当前拦截器
		Interceptor interceptor = interceptors.get(index);
		// 调用当前拦截器的intercept并传入下一个拦截器
		Response response = interceptor.intercept(next);

		// Confirm that the next interceptor made its required call to chain.proceed().
		if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
			throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once");
		}

		// Confirm that the intercepted response isn't null.
		if (response == null) {
			throw new NullPointerException("interceptor " + interceptor + " returned null");
		}

		if (response.body() == null) {
			throw new IllegalStateException("interceptor " + interceptor + " returned a response with no body");
		}

		return response;
	}
}
